﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;

namespace Rosetta.Composition
{
    public sealed class Template : IDisposable, INotifyPropertyChanged
    {
        // TODO: Load from xml file
        // TODO: Find missing argument types

        #region Fields

        private readonly List<Node> _Nodes = new List<Node>();

        #endregion

        #region Events

        public event PropertyChangedEventHandler PropertyChanged;

        #endregion

        #region Constructors

        public Template([NotNull] IContainer container)
        {
            if (container == null)
            {
                throw new ArgumentNullException("container");
            }

            Container = container;
        }

        #endregion

        #region Properties

        [NotNull]
        public IContainer Container { get; private set; }

        [NotNull]
        public IEnumerable<InputterNode> InputNodes
        {
            get
            {
                return _Nodes.OfType<InputterNode>();
            }
        }

        [NotNull]
        public IEnumerable<Node> Nodes
        {
            get
            {
                return _Nodes.AsReadOnly();
            }
        }

        #endregion

        #region Public Methods

        public void Accept([NotNull] ITemplateVisitor visitor)
        {
            if (visitor == null)
            {
                throw new ArgumentNullException("visitor");
            }

            foreach (var inputterNode in InputNodes)
            {
                inputterNode.Accept(visitor);
            }
        }

        [NotNull]
        public InputterNode AddInputNode([NotNull] IInputter inputter, [NotNull] string methodName)
        {
            if (inputter == null)
            {
                throw new ArgumentNullException("inputter");
            }
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentNullException("methodName");
            }

            var node = new InputterNode(this, inputter, methodName);
            _Nodes.Add(node);
            SendPropertyChanged("InputNodes");
            return node;
        }

        [NotNull]
        public InputterNode AddInputNode([NotNull] string addInName, [NotNull] string methodName)
        {
            if (addInName == null)
            {
                throw new ArgumentNullException("addInName");
            }

            var inputter = Container.GetInputter(addInName);
            if (inputter == null)
            {
                throw new ArgumentException(string.Format("Inputter add-in '{0}' not found.", addInName), "addInName");
            }

            return AddInputNode(inputter, methodName);
        }

        [NotNull]
        public Type[] GetCurrentReturnTypes()
        {
            return _Nodes.Where(n => n.CanHaveChildren).SelectMany(n => n.MethodEntry.ReturnTypes).Distinct().ToArray();
        }

        public void Dispose()
        {
            foreach (var node in _Nodes)
            {
                node.Dispose();
            }
            _Nodes.Clear();
            PropertyChanged = null;
// ReSharper disable AssignNullToNotNullAttribute
            Container = null;
// ReSharper restore AssignNullToNotNullAttribute
        }

        #endregion

        #region Internal Methods

        /// <summary>
        /// Can only be used by AddChild for now so we always have a complete tree.
        /// </summary>
        [NotNull]
        internal Node CreateNode([NotNull] string addInName, [NotNull] string methodName)
        {
            if (string.IsNullOrEmpty(addInName))
            {
                throw new ArgumentNullException("addInName");
            }
            if (string.IsNullOrEmpty(methodName))
            {
                throw new ArgumentNullException("methodName");
            }

            IAddIn addIn = Container.GetAddIn(addInName);

            Node node = null;

            var converter = addIn as IConverter;
            if (converter != null)
            {
                node = new ConverterNode(this, converter, methodName);
            }
            else
            {
                var outputter = addIn as IOutputter;
                if (outputter != null)
                {
                    node = new OutputterNode(this, outputter, methodName);
                }
            }

            if (node == null)
            {
                throw new ArgumentException(string.Format("Add-in '{0}' not found.", addInName), "addInName");
            }

            _Nodes.Add(node);
            return node;
        }

        #endregion

        #region Private Methods

        private void SendPropertyChanged(string propertyName)
        {
            var handler = PropertyChanged;
            if (handler != null)
            {
                handler(this, new PropertyChangedEventArgs(propertyName));
            }
        }

        #endregion
    }
}